/*
 * Copyright 2007 Nathan Smith
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.geeksmithology.io;

import java.io.*;

/**
 * Several utility methods for dealing with streams.
 */
public class Streams {
    /**
     * Get the contents of the provided InputStream as a byte array.  No memory will be used for a temporary buffer.
     * <p/>
     * <b>NOTE:</b> If the specified InputStream has already had one of the read() methods called on it, then only
     * the remaining bytes will be read and returned by this method.
     *
     * @param inputStream An InputStream from which to retrieve the bytes.  Cannot be null.
     * @return A byte array containing all bytes that can be read from the specified InputStream.
     * @throws IOException If there are any errors reading from the InputStream.
     */
    public static byte[] toBytes(InputStream inputStream) throws IOException {
        if (inputStream == null) {
            throw new IllegalArgumentException("InputStream for toBytes cannot be null.");
        }

        ByteArrayOutputStream outputStream = null;

        try {
            outputStream = new ByteArrayOutputStream();
            copyBytes(inputStream, outputStream);
            return outputStream.toByteArray();
        } finally {
            closeStream(outputStream);
        }
    }

    /**
     * Get the contents of the provided InputStream as a byte array, converting any IOExceptions to RuntimeExceptions.
     * This method is convenient for places where an IOException cannot be sensibly handled, so it avoids requiring
     * a try/catch block. No memory will be allocated for an IO buffer.
     * <p/>
     * <b>NOTE:</b> If the specified InputStream has already had one of the read() methods called on it, then only
     * the remaining bytes will be read and returned by this method.
     *
     * @param inputStream An InputStream from which to retrieve the bytes.  Cannot be null.
     * @return A byte array containing all bytes that can be read from the specified InputStream.
     * @throws RuntimeException If there are any IOExceptions reading from the InputStream.
     */
    public static byte[] toBytesUnchecked(InputStream inputStream) {
        try {
            return toBytes(inputStream);
        } catch (IOException e) {
            throw new RuntimeException("IOException converting stream to bytes: " + e.getMessage(), e);
        }
    }

    /**
     * Get the contents of the provided InputStream as a byte array, using the provided byte array as an IO buffer.
     * Certain operations (especially if the sockets are over a network connection) can perform significantly
     * faster using a buffer.
     * <p/>
     * <b>NOTE:</b> If the specified InputStream has already had one of the read() methods called on it, then only
     * the remaining bytes will be read and returned by this method.
     *
     * @param inputStream An InputStream from which to retrieve the bytes.  Cannot be null.
     * @param buffer      A byte array to use as and IO buffer.  Length must be > 0.  Cannot be null.
     * @return A byte array containing all bytes that can be read from the specified InputStream.
     * @throws IOException If there are any errors reading from the InputStream.
     */
    public static byte[] toBytes(InputStream inputStream, byte[] buffer) throws IOException {
        if (inputStream == null) {
            throw new IllegalArgumentException("InputStream cannot be null.");
        }
        if (buffer == null) {
            throw new IllegalArgumentException("Buffer cannot be null.");
        }

        if (buffer.length == 0) {
            throw new IllegalArgumentException("Buffer length must be > 0");
        }

        ByteArrayOutputStream byteArrayOutputStream = null;

        try {
            byteArrayOutputStream = new ByteArrayOutputStream();
            copyBytes(inputStream, byteArrayOutputStream, buffer);
            return byteArrayOutputStream.toByteArray();
        } finally {
            closeStream(byteArrayOutputStream);
        }
    }

    /**
     * Get the contents of the provided InputStream as a byte array, using the provided byte array as an IO buffer,
     * and converting any IOExceptions to RuntimeExceptions.  Certain operstaions (especially if the InputStream
     * is over a network connection) can perform significantly faster using a buffer.
     * <p/>
     * <b>NOTE:</b>  If the provided InputStream has already had one of the read() methods called on it, then only
     * the remaining bytes will be read and returned by this method.
     *
     * @param inputStream An InputStream from which to retrieve the bytes.  Cannot be null.
     * @param buffer      A byte array to be used as an IO buffer.  Length must be > 0.  Cannot be null.
     * @return A byte array containing all bytes than can be read from the provided InputStream.
     * @throws RuntimeException if there are any IOExceptions encountered when reading from the InputStream.
     */
    public static byte[] toBytesUnchecked(InputStream inputStream, byte[] buffer) {
        try {
            return toBytes(inputStream, buffer);
        } catch (IOException e) {
            throw new RuntimeException("IOException converting stream to bytes: " + e.getMessage(), e);
        }
    }

    /**
     * Reads all remaining bytes from the provided InputStream and writes them to the provided OutputStream.  No
     * internal memory is allocated for a buffer with this method.
     * <p/>
     * <b>Note:</b> if the source has already been read from, then only the bytes from the current position until
     * the end of the stream will be copied.
     * <b>Note:</b> if the target has already been written to, then the bytes read from the InputStream will be
     * appended to the existing stream data.
     *
     * @param source An InputStream from which to read the bytes to be copied.  Cannot be null.
     * @param target An OutputStream to which to write the bytes to be copied.  Cannot be null.
     * @throws IOException If there are any stream errors copying the bytes.
     */
    public static void copyBytes(InputStream source, OutputStream target) throws IOException {
        if (source == null) {
            throw new IllegalArgumentException("Source InputStream cannot be null for copy.");
        }

        if (target == null) {
            throw new IllegalArgumentException("Target OutputStream cannot be null for copy.");
        }

        for (int byteRead = source.read(); byteRead > -1; byteRead = source.read()) {
            target.write(byteRead);
        }
    }

    /**
     * Reads all remaining bytes from the provided InputStream and writes them to the provided OutputStream,
     * wrapping any IOExceptions as RuntimeExceptions.  This method is convenient for code where an IOException
     * is unrecoverable and you don't want to require a try/catch block.  No internal memory is allocated for a
     * buffer with this method.
     * <p/>
     * <b>Note:</b> if the source has already been read from, then only the bytes from the current position until
     * the end of the stream will be copied.
     * <b>Note:</b> If the target has already been written to, then the bytes read from the InputStream will be
     * appended to the existing stream data.
     *
     * @param source An InputStream from which to read the bytes to be copied.  Cannot be null.
     * @param target An OutputStream to which to write the bytes to be copied.  Cannot be null.
     * @throws RuntimeException If there are any IOExceptions thrown while copying the bytes.
     */
    public static void copyBytesUnchecked(InputStream source, OutputStream target) {
        try {
            copyBytes(source, target);
        } catch (IOException e) {
            throw new RuntimeException("IOException copying bytes: " + e.getMessage(), e);
        }
    }

    /**
     * Reads all remaining bytes from the provided InputStream and writes them to the provided OutputStream, using
     * the provided byte array as a buffer.  Note that for certain operations (like when the streams are over a
     * network) using a buffer can result in significantly faster performance.
     * <p/>
     * <b>Note:</b> if the source has already been read from, then only the bytes from the current position until
     * the end of the stream will be copied.
     * <b>Note:</b> if the target has already been written to, then the bytes read from the InputStream will be
     * appended to the existing stream data.
     *
     * @param source An InputStream from which to read the bytes to be copied.  Cannot be null.
     * @param target An OutputStream to which to write the bytes to be copied.  Cannot be null.
     * @param buffer A byte array to use as an IO buffer during the copy.  Must have length > 0 and cannot be null.
     * @throws IOException If there are any stream errors copying the bytes.
     */
    public static void copyBytes(InputStream source, OutputStream target, byte[] buffer) throws IOException {
        if (source == null) {
            throw new IllegalArgumentException("Source for copy cannot be null.");
        }

        if (target == null) {
            throw new IllegalArgumentException("Target for copy cannot be null.");
        }

        if (buffer == null) {
            throw new IllegalArgumentException("Buffer for copy cannot be null.");
        }

        if (buffer.length == 0) {
            throw new IllegalArgumentException("Buffer for copy must have a length > 0.");
        }

        for (int bytesRead = source.read(buffer); bytesRead > 0; bytesRead = source.read(buffer)) {
            target.write(buffer, 0, bytesRead);
        }
    }

    /**
     * Reads all remaining bytes from the provided InputStream and writes them to the provided OutputStream, using
     * the provided byte array as a buffer and wrapping any IOExceptions as RuntimeExceptions.  For code where an
     * IOException is an unrecoverable error, this method may be used to remove the need for a try/catch block.
     * Note that for certain operations (like when the streams are over a network) using a buffer can result in
     * significantly faster performance.
     * <p/>
     * <b>Note:</b> if the source has already been read from, then only the bytes from the current position until
     * the end of the stream will be copied.
     * <b>Note:</b> if the target has already been written to, then the bytes read from the InputStream will be
     * appended to the existing stream data.
     *
     * @param source An InputStream from which to read the bytes to be copied.  Cannot be null.
     * @param target An OutputStream to which to write the bytes to be copied.  Cannot be null.
     * @param buffer A byte array to use as an IO buffer during the copy.  Must have length > 0 and cannot be null.
     * @throws RuntimeException If an IOException is called during the copy.
     */
    public static void copyBytesUnchecked(InputStream source, OutputStream target, byte[] buffer) {
        try {
            copyBytes(source, target, buffer);
        } catch (IOException e) {
            throw new RuntimeException("IOException copying bytes: " + e.getMessage(), e);
        }
    }

    /**
     * Convenience method for closing an InputStream when an IOException is to be considered as an unrecoverable
     * error.  This will check for nulls and will convert any IOExceptions to chained RuntimeExceptions.
     *
     * @param inputStream The InputStream to close.
     */
    public static void closeStream(InputStream inputStream) {
        try {
            if (inputStream != null) {
                inputStream.close();
            }
        } catch (IOException e) {
            throw new RuntimeException("IOException closing InputStream: " + e.getMessage(), e);
        }
    }

    /**
     * Convenience method for closing an OutputStream when an IOException is to be considered as an unrecoverable
     * error.  This will check for nulls and will convert any IOExceptions to chained RuntimeExceptions.
     *
     * @param outputStream The OutputStream to close.
     */
    public static void closeStream(OutputStream outputStream) {
        try {
            if (outputStream != null) {
                outputStream.close();
            }
        } catch (IOException e) {
            throw new RuntimeException("IOException closing OutputStream: " + e.getMessage(), e);
        }

    }
}
